package com.hero.take.util;

import com.esotericsoftware.reflectasm.FieldAccess;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.Validator;

import javax.validation.ValidationException;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

import static org.springframework.beans.BeanUtils.getPropertyDescriptor;
import static org.springframework.beans.BeanUtils.getPropertyDescriptors;

@Slf4j
public class BeanUtil {

    private static ObjectMapper mapper = new ObjectMapper();

    static ThreadLocal<Map<Class<?>, Map<String, Field>>> fieldCache = new ThreadLocal<>();

    static ThreadLocal<Map<Class<?>, FieldAccess>> proxyCache = new ThreadLocal<>();

    static {
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
    }

    public static <T> T json2Object(String json, Class<T> c) {
        try {
            return mapper.readValue(json, c);
        } catch (IOException e) {
            log.error("json 转换 {} 异常", c.getName(), e);
        }
        return null;
    }

    public static Object json2Object(String json, Class<?> c1, Class<?>... c2) {
        try {
            JavaType javaType = mapper.getTypeFactory().constructParametricType(c1, c2);
            return mapper.readValue(json, javaType);
        } catch (IOException e) {
            log.error("json 转换 {} 异常", c1.getName(), e);
        }
        return null;
    }

    public static String object2Json(Object o) {
        try {
            return mapper.writeValueAsString(o);
        } catch (JsonProcessingException e) {
            log.error("object:[{}] 转换 string 异常", o, e);
        }
        return null;
    }

    public static void validateJSR303(Validator validator, Object o) throws ValidationException {
        BindingResult result = new BeanPropertyBindingResult(o, o.getClass().getName());
        validator.validate(o, result);
        List<ObjectError> allErrors = result.getAllErrors();
        if (allErrors != null && allErrors.size() > 0) {
            String errorMessage = "";
            for (ObjectError error : allErrors) {
                errorMessage += "【" + error.getDefaultMessage() + "】";
            }
            throw new ValidationException(errorMessage);
        }
    }

    public static void setNull(Object src, String filedName) throws Exception {
        setValue(src, filedName, null);
    }

    public static void setValue(Object src, String filedName, Object value) throws Exception {
        if (PropertyUtils.isWriteable(src, filedName)) {
            BeanUtils.copyProperty(src, filedName, value);
        }
    }

    public static void copy(Object src, Object desc) {
        if(src == null){
            return;
        }
        PropertyDescriptor[] propertys = PropertyUtils.getPropertyDescriptors(desc);
        if (propertys.length < 1) {
            return;
        }

        String descName, srcName;
        PropertyDescriptor _property;
        Field descField;
        Object value;
        for (int i = 0; i < propertys.length; i++) {
            _property = propertys[i];

            if ("class".equals(_property.getName())) {
                continue;
            }
            descName = _property.getName();
            srcName = descName;
            descField = findField(desc.getClass(), descName, null);
            if (descField == null || !PropertyUtils.isWriteable(desc, descName)) {
                continue;
            }

            if (descField.isAnnotationPresent(FiledAlias.class)) {
                try {
                    srcName = Optional.ofNullable(descField.getAnnotation(FiledAlias.class).value())
                            .map(alias -> Arrays.stream(alias)
                                    .filter(_srcName -> PropertyUtils.isReadable(src, _srcName))
                                    .collect(Collectors.toList()))
                            .filter(_srcName -> _srcName.size() == 1).map(_srcName -> _srcName.get(0)).orElse(srcName);
                } catch (Exception e) {
                    log.warn("bean copy error, 解析@FiledAlias 失败", e);
                }
            }

            try {
                value = PropertyUtils.getProperty(src, srcName);
                if (descField.isAnnotationPresent(FiledAlias.class)) {
                    FiledAlias filedAlias = descField.getAnnotation(FiledAlias.class);
                    boolean isCollection = filedAlias.isCollection();
                    boolean isGetCollectionSize = filedAlias.isGetCollectionSize();
                    int index = filedAlias.index();
                    String subAlias = filedAlias.subAlias();

                    if (isCollection && isGetCollectionSize) {
                        value = ((Collection<?>) value).size();
                    }
                    if (isCollection && StringUtils.isNotBlank(subAlias)) {
                        Object[] _tmp = ((Collection<?>) value).toArray();
                        if (_tmp != null && _tmp.length > index) {
                            Object subObj = _tmp[index];
                            value = PropertyUtils.getProperty(subObj, filedAlias.subAlias());
                        }
                    }
                }
                if (descField.isAnnotationPresent(DateStringFormat.class) && value != null && value instanceof String) {
                    DateStringFormat format = descField.getAnnotation(DateStringFormat.class);
                    String srcF = format.srcFormat();
                    String descF = format.descFormat();
                    LocalDate srcD = LocalDate.parse((String) value, DateTimeFormatter.ofPattern(srcF));
                    value = srcD.format(DateTimeFormatter.ofPattern(descF));
                }

                if (value == null) {
                    continue;
                }
                if (descField != null && "interface java.util.List".equals(descField.getType().toString())) {
                    Field srcField = findField(src.getClass(), srcName, null);
                    Class<?> srcClass = (Class<?>) ((ParameterizedType) srcField.getGenericType())
                            .getActualTypeArguments()[0];
                    Class<?> descClass = (Class<?>) ((ParameterizedType) descField.getGenericType())
                            .getActualTypeArguments()[0];
                    if (srcClass != descClass) {
                        value = copyList((List<?>) value, descClass);
                    }
                }

                copyProperty(desc, descName, value);
            } catch (Exception e) {
                log.error("bean copy error. 复制属性失败, src={}", object2Json(src));
            }
        }
    }

    private static void copyProperty(Object desc, String descName, Object value) {
        try {
            Class<?> descClassz = desc.getClass();
            Map<Class<?>, FieldAccess> cache = proxyCache.get();
            if (cache == null) {
                cache = new WeakHashMap<>();
                proxyCache.set(cache);
            }
            FieldAccess access = cache.get(descClassz);
            if (access == null) {
                access = FieldAccess.get(desc.getClass());
                cache.put(descClassz, access);
            }
            access.set(desc, descName, value);
        } catch (Exception e) {
            try {
                BeanUtils.copyProperty(desc, descName, value);
            } catch (Exception e1) {
            }
        }
    }

    public static <S, D> List<D> copyList(List<S> src, Class<D> descClass) {
        return copyList(src, descClass, null);
    }

    public static <S, D> List<D> copyList(List<S> src, Class<D> descClass, BeanCopyCallBack<S, D> callBack) {
        if (src == null) {
            return new ArrayList<>();
        }

        List<D> desc = new ArrayList<>(src.size());
        for (S source : src) {
            try {
                D t = descClass.newInstance();
                copyProperties(source, t,null);
                desc.add(t);
                if (callBack != null) {
                    callBack.callBack(source, t);
                }
            } catch (Exception e) {
                log.error("bean copy error. 复制List失败 ", e);
            }
        }
        return desc;
    }

    public static Field findField(Class<?> clazz, String name, Class<?> type) {
        Map<Class<?>, Map<String, Field>> cache = fieldCache.get();
        if (cache == null) {
            cache = new WeakHashMap<>();
            fieldCache.set(cache);
        }
        Map<String, Field> fieldMap = cache.get(clazz);
        if (fieldMap == null) {
            fieldMap = new WeakHashMap<>();
            cache.put(clazz, fieldMap);
        }
        Field field = fieldMap.get(name);
        if (field == null) {
            field = getField(clazz, name, type);
            fieldMap.put(name, field);
        }
        return field;
    }

    private static Field getField(Class<?> clazz, String name, Class<?> type) {
        Class<?> searchType = clazz;
        while (!Object.class.equals(searchType) && searchType != null) {
            Field[] fields = searchType.getDeclaredFields();
            for (Field field : fields) {
                if ((name == null || name.equals(field.getName())) && (type == null || type.equals(field.getType()))) {
                    field.setAccessible(true);
                    return field;
                }
            }
            searchType = searchType.getSuperclass();
        }
        return null;
    }

    public static boolean filedNotNull(Object o) {
        if (o == null) {
            return false;
        } else if (o instanceof String && "".equals(((String) o).trim())) {
            return false;
        } else if (o instanceof List && ((List<?>) o).size() == 0) {
            return false;
        }
        return true;
    }

    public static boolean filedNull(Object o) {
        return !filedNotNull(o);
    }

    public static Map<String, Object> toMap(Object obj) {
        if (obj == null) {
            return new HashMap<>(1);
        }

        try {
            Map<String, String> map = BeanUtils.describe(obj);
            return map.entrySet().stream().filter(_entry -> _entry.getValue() != null)
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        } catch (Exception e) {
            log.error("bean ==> map error.", e);
        }

        return null;
    }

    public static boolean includedFiledInClass(Class<?> clazz, String name) {
        return findField(clazz, name, null) != null;
    }

    public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");
        Class<?> actualEditable = target.getClass();
        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
        for (PropertyDescriptor targetPd : targetPds) {
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null) {
                if (ignoreList == null || !ignoreList.contains(targetPd.getName())) {
                    PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                    if (sourcePd != null) {
                        Method readMethod = sourcePd.getReadMethod();
                        if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                            try {
                                if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                    readMethod.setAccessible(true);
                                }

                                Object value = readMethod.invoke(source);
                                if (value != null) {
                                    if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                        writeMethod.setAccessible(true);
                                    }
                                    writeMethod.invoke(target, value);
                                }

                            } catch (Throwable var15) {
                                throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
                            }
                        }
                    }
                }
            }
        }
    }
}